﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LinqToLfsWorld.Attributes;
using LinqToLfsWorld.Collections;
using LinqToLfsWorld.Interface;
using LinqToLfsWorld.Linq;

namespace LinqToLfsWorld.Queries
{

	/// <summary>
	/// Base class for searches where the search results can be filtered on the server.
	/// </summary>
	/// <typeparam name="EntityType">The type of entity being queried</typeparam>
	public abstract class LfsWorldQuery<EntityType> : Linq.QueryProvider, ILfsWorldQuery
		where EntityType: ILfsWorldEntity
	{

		private class ExpressionCandidates : Dictionary<string, PubstatQueryableAttribute> { }


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="LfsWorldQuery&lt;EntityType&gt;"/> class.
		/// </summary>
		/// <param name="context">The context.</param>
		public LfsWorldQuery(LfsWorldContext context)
		{
			Context = context;
		}

		#endregion


		#region ISearchBase Members

		/// <summary>
		/// Gets or sets the context.
		/// </summary>
		public LfsWorldContext Context { get; set; }
		
		#endregion


		#region Subclass Interface

		/// <summary>
		/// Gets or sets the expression values collection. This collection will contain the property names and values which
		/// were found in the input expression.
		/// </summary>
		protected ExpressionValueCollection ExpressionValues { get; private set; }

		/// <summary>
		/// Gets the URL used to query LfsWorld.
		/// </summary>
		/// <returns></returns>
		protected abstract string GetUrl();

		/// <summary>
		/// Processes the server response
		/// </summary>
		/// <param name="responseText">The response text.</param>
		protected abstract IEnumerable<EntityType> ProcessResponse(string responseText);

		#endregion


		#region QueryProvider Implementation

		/// <summary>
		/// The selection method being used (Single, First, Last, etc)
		/// </summary>
		private static string SelectionMethod { get; set; }

		/// <summary>
		/// Gets the query text from the specified expression.
		/// </summary>
		/// <param name="expression">The expression.</param>
		public override string GetQueryText(Expression expression)
		{
			expression = GetBaseExpression(expression);

			ExpressionCandidates candidates = BuildCandidates();

			// Create the expression finder and find all of our queryable properties
			PropertyExpressionFinder finder = new PropertyExpressionFinder();

			try
			{
				finder.FindExpressionValues(expression, candidates.Keys.ToArray());
			}
			catch (NotSupportedException ex)
			{
				throw new PubstatQueryException(ex.Message);
			}

			ValidateCandidates(candidates, finder);

			ExpressionValues = finder.OutputValues;

			return GetUrl();
		}

		/// <summary>
		/// Executes the specified expression.
		/// </summary>
		/// <param name="expression">The expression.</param>
		public override object Execute(Expression expression)
		{
			string url = GetQueryText(expression);
			LfsWorldResponse response = LfsWorldRequest.Instance.MakeRequest(url);
			IEnumerable<EntityType> results = null;

			try
			{
				if (response.ContentLength > 0)
				{
					results = ProcessResponse(response.ResponseData);
				}
				else
				{
					throw new PubstatRequestException(url, null, "The server didn't return a response");
				}

				return SelectResults(results);

			}
			catch
			{
				throw new PubstatRequestException(url, response.ResponseData, "Error generating request: " + response.ResponseData);
			}

		}

		#endregion


		#region Private Implementation

		/// <summary>
		/// Selects the results based on the selection method being used (First, Last, Single, etc)
		/// </summary>
		/// <param name="results">The full set of results</param>
		private object SelectResults(IEnumerable<EntityType> results)
		{

			if (string.IsNullOrEmpty(SelectionMethod))
				return results;

			switch (SelectionMethod)
			{
				case "Single":
					return results.Single();
					
				case "SingleOrDefault":
					return results.SingleOrDefault();

				case "First":
					return results.First();

				case "FirstOrDefault":
					return results.FirstOrDefault();

				case "Last":
					return results.Last();

				case "LastOrDefault":
					return results.LastOrDefault();

				case "Count":
					return results.Count();

				default:
					throw new PubstatQueryException("Invalid selection method");

			}
		}

		/// <summary>
		/// Finds the base expression so that queryable properties and their values can be found.
		/// </summary>
		/// <param name="expression">The expression.</param>
		/// <exception cref="PubstatQueryException">Either an unsupported method was used, or the
		/// query is trying to select an anonymous type.</exception>
		private static Expression GetBaseExpression(Expression expression)
		{
			// Evaluate all the local parts of the query that we can
			expression = Linq.Evaluator.PartialEval(expression);

			// Find the lambda expression
			if (expression is MethodCallExpression)
			{
				MethodCallExpression methodExpr = (MethodCallExpression)expression;

				switch (methodExpr.Method.Name)
				{

					case "Select":
						{
							// Make sure the return type of this method is EntityType - anonymous selections not supported
							if (((UnaryExpression)methodExpr.Arguments[1]).Operand.GetType() !=
								typeof(Expression<Func<EntityType, EntityType>>))
							{
								throw new PubstatQueryException("Anonymous Select not supported");
							}

							return (LambdaExpression)((UnaryExpression)methodExpr.Arguments[1]).Operand;
						}

					case "Single":
					case "SingleOrDefault":
					case "First":
					case "FirstOrDefault":
					case "Last":
					case "LastOrDefault":
					case "Count":
						{
							SelectionMethod = methodExpr.Method.Name;
							return GetBaseExpression(methodExpr.Arguments[0]);
						}

					case "Where":
							return (LambdaExpression)((UnaryExpression)methodExpr.Arguments[1]).Operand;

					default:
						throw new PubstatQueryException("Method not supported: " + methodExpr.Method.Name);

				}

			}
			else
			{
				return expression;
			}
		}

		/// <summary>
		/// Builds a list of expression candidate properties, determined by the properties of an ILfsWorldEntity
		/// implementation which are attributed with the <see cref="LinqToLfsWorld.Attributes.PubstatQueryableAttribute" />.
		/// </summary>
		private static ExpressionCandidates BuildCandidates()
		{
			Type entType = typeof(EntityType);

			// Get each of the entities properties
			PropertyInfo[] entityProps = typeof(EntityType).GetProperties();

			ExpressionCandidates candidates = new ExpressionCandidates();

			// Build up a dictionary of propertynames and the pubstat queryable attribute, so we can easily index
			// them later.
			for (int i = 0; i < entityProps.Length; i++)
			{
				PropertyInfo prop = entityProps[i];

				object[] attributes = prop.GetCustomAttributes(typeof(Attributes.PubstatQueryableAttribute), true);

				if (attributes != null && attributes.Length == 1)
				{
					candidates.Add(prop.Name, (PubstatQueryableAttribute)attributes[0]);
				}
				else
				{
					if (attributes.Length > 1)
						throw new Exception("Can only specify PubstatQueryableAttribute once on an entity.");
				}
			}
			return candidates;
		}

		/// <summary>
		/// Checks to see whether all the required expression parameters were supplied or not.
		/// </summary>
		/// <param name="candidates">The candidates collection</param>
		/// <param name="finder">The expression property finder</param>
		private void ValidateCandidates(ExpressionCandidates candidates, PropertyExpressionFinder finder)
		{
			foreach (var exprValue in candidates)
			{
				if (exprValue.Value.Required == true &&
					((!finder.OutputValues.ContainsKey(exprValue.Key))
					|| finder.OutputValues[exprValue.Key].Value == null))
				{
					throw new PubstatQueryException(exprValue.Key + " parameter must be provided in the query");
				}
			}
		}

		#endregion

	}
}
